/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.modeling;

import java.util.ArrayList;

import javax.media.opengl.GL;

import de.akob.mathematics.Cuboid;
import de.akob.mathematics.MathVector;
import de.akob.mathematics.Mathematics;

/**
 * A model class using OpenGL. Supported features:
 * <p> - three dimensional vertices
 * <p> - edges (with references to corresponding vertices)
 * <p> - triangular faces (with references to corresponding edges and vertices)
 * <p> - face/edge/vertex normals
 */
public class Model {
	/**
	 * Type to indicate the kind of faces of a mesh.
	 */
	public enum MeshType {
		MESH_MIXED, MESH_TRIANGLES, MESH_QUADS, MESH_TRIANGLE_STRIP
	}
	
	// mesh data
	private final ArrayList<Vertex> vertices;
	private final ArrayList<Face>   faces                     = new ArrayList<Face>();
	private final EdgeDB            edgeDB                    = new EdgeDB();
	private Cuboid                  boundingBox;
	
	// type of mesh (whether it consists of triangles, quads, triangle fans, ...)
	private MeshType                meshType                  = null;
	
	// display lists for model
	private int                     displayList_vertexNormals = 0;
	private int                     displayList_faceNormals   = 0;
	private int                     displayList_current       = 0;
	
	/**
	 * Constructs a model using the given vertices and faces.
	 * 
	 * @param vertices list of vertices
	 * @param faces list of faces (two dimensional arrays of indexes corresponding to entries in <code>vertices</code>;
	 *            counterclockwise)
	 */
	public Model(ArrayList<Vertex> vertices, ArrayList<ArrayList<Integer>> faces) {
		this.vertices = vertices;
		addFaces(faces);
		computeMeshType();
		computeBoundingBox();
	}
	
	/**
	 * Constructs a model using the given vertices, faces and (face) normals.
	 * 
	 * @param vertices list of vertices
	 * @param faces list of faces (two dimensional arrays of indexes corresponding to entries in <code>vertices</code>;
	 *            counterclockwise)
	 * @param normals per face normals (have to be normalized; there has to be a normal for every face)
	 * @throws IllegalArgumentException if size of <code>faces</code> and <code>normals</code> do not match
	 */
	public Model(ArrayList<Vertex> vertices, ArrayList<ArrayList<Integer>> faces, ArrayList<MathVector> normals) {
		if (normals.size() != faces.size())
			throw new IllegalArgumentException("number of normals must match number of faces");
		this.vertices = vertices;
		addFaces(faces);
		computeMeshType();
		computeBoundingBox();
		for (int i = 0; i < faces.size(); i++)
			this.faces.get(i).setNormal(normals.get(i));
		computeEdgeNormals();
		computeVertexNormals();
	}
	
	/**
	 * Adds the faces (and the corresponding edges) given by the two-dimensional integer array. The integers are
	 * interpreted as indices of the vertices defining the face (the corresponding vertices have to be loaded already).
	 * This method should be used only in a constructor before any call to <code>computeMeshTyp</code>,
	 * <code>computeEdgeNormals</code> or <code>computeVertexNormals</code>.
	 * 
	 * @param newFaces two-dimensional array list of indices defining the faces
	 */
	private void addFaces(ArrayList<ArrayList<Integer>> newFaces) {
		Face currFace;
		ArrayList<Integer> currFaceIndices;
		for (int i = 0; i < newFaces.size(); i++) {
			currFaceIndices = newFaces.get(i);
			Vertex[] faceVertices = new Vertex[currFaceIndices.size()];
			for (int j = 0; j < currFaceIndices.size(); j++)
				faceVertices[j] = vertices.get(currFaceIndices.get(j));
			currFace = new Face(edgeDB, faceVertices);
			faces.add(currFace);
		}
	}
	
	/**
	 * Computes the bounding box of the model (aligned to the standard axis).
	 */
	private void computeBoundingBox() {
		double xMin = Double.POSITIVE_INFINITY;
		double yMin = Double.POSITIVE_INFINITY;
		double zMin = Double.POSITIVE_INFINITY;
		double xMax = Double.NEGATIVE_INFINITY;
		double yMax = Double.NEGATIVE_INFINITY;
		double zMax = Double.NEGATIVE_INFINITY;
		for (Vertex vertex : vertices) {
			xMin = Math.min(xMin, vertex.get(1));
			yMin = Math.min(yMin, vertex.get(2));
			zMin = Math.min(zMin, vertex.get(3));
			xMax = Math.max(xMax, vertex.get(1));
			yMax = Math.max(yMax, vertex.get(2));
			zMax = Math.max(zMax, vertex.get(3));
		}
		boundingBox = new Cuboid(xMin, xMax, yMin, yMax, zMin, zMax);
	}
	
	/**
	 * Computes edge normals from the given face normals. Edge normals are given by the (normalized) sum of neighbouring
	 * face normals.
	 */
	private void computeEdgeNormals() {
		for (Face face : faces) {
			for (Edge edge : face.getEdges()) {
				if (edge.getNormal() == null)
					edge.setNormal(face.getNormal().clone());
				else
					edge.getNormal().add(face.getNormal());
			}
		}
		for (Edge edge : edgeDB.getEdges())
			edge.getNormal().normalize();
	}
	
	/**
	 * Computes the mesh type of this mesh.
	 */
	private void computeMeshType() {
		boolean checkerVariable;
		
		// check for mesh of triangles
		checkerVariable = true;
		for (Face face : faces) {
			if (face.vertexNumber() != 3) {
				checkerVariable = false;
				break;
			}
		}
		if (checkerVariable) {
			// check for triangle strip
			checkerVariable = true;
			for (int i = 0; i < faces.size(); i++) {
				Face face = faces.get(i);
				if ((i % 2) == 0) {
					if (face.getVertices()[0] != vertices.get(i) || face.getVertices()[1] != vertices.get(i + 1)
					        || face.getVertices()[2] != vertices.get(i + 2)) {
						checkerVariable = false;
						break;
					}
				} else {
					if (face.getVertices()[0] != vertices.get(i + 1)
					        || face.getVertices().clone()[1] != vertices.get(i)
					        || face.getVertices().clone()[2] != vertices.get(i + 2)) {
						checkerVariable = false;
						break;
					}
				}
			}
			if (checkerVariable) {
				meshType = MeshType.MESH_TRIANGLE_STRIP;
				return;
			}
			
			// mesh of triangles, but no triangle strip
			meshType = MeshType.MESH_TRIANGLES;
			return;
		}
		
		// check for mesh of quads
		checkerVariable = true;
		for (Face face : faces) {
			if (face.vertexNumber() != 4) {
				checkerVariable = false;
				break;
			}
		}
		if (checkerVariable) {
			meshType = MeshType.MESH_QUADS;
			return;
		}
		
		// can not guarantee a special kind of mesh
		meshType = MeshType.MESH_MIXED;
	}
	
	/**
	 * Computes vertex normals from the given face normals. Face normals are given by the (normalized) weighted sum of
	 * neighbouring face normals. The weight factor is determined by the incident angle of the corresponding face.
	 */
	private void computeVertexNormals() {
		for (Face face : faces) {
			Vertex v1, v2, v3;
			MathVector direction1, direction2;
			double weight;
			for (int i = 1; i <= face.getVertexNumber(); i++) {
				v1 = face.getVertices()[i - 1];
				v2 = face.getVertices()[i % face.getVertices().length];
				v3 = face.getVertices()[(i + 1) % face.getVertices().length];
				direction1 = Mathematics.sub(v1, v2);
				direction2 = Mathematics.sub(v3, v2);
				direction1.normalize();
				direction2.normalize();
				weight = Math.acos(Mathematics.mul(direction1, direction2));
				if (v2.getNormal() == null) {
					v2.setNormal(face.getNormal().clone());
					v2.getNormal().mul(weight);
				} else
					v2.getNormal().add(Mathematics.mul(weight, face.getNormal()));
			}
		}
		for (Vertex vertex : vertices)
			vertex.getNormal().normalize();
	}
	
	/**
	 * Draws the model using OpenGL. Make sure to initialize the model before calling this method.
	 * 
	 * @param gl OpenGL object used for drawing
	 */
	public void draw(GL gl) {
		gl.glCallList(displayList_current);
	}
	
	/**
	 * Draws the model with some (customizable) extra information.
	 * 
	 * @param drawVertexNormal whether to draw vertex normals (red)
	 * @param drawEdgeNormal whether to draw edge normals (green)
	 * @param drawFaceNormal whether to draw face normals (blue)
	 * @param normalLength length of the normals to be drawn
	 */
	public void draw_debug(GL gl, boolean drawVertexNormal, boolean drawEdgeNormal, boolean drawFaceNormal,
	        double normalLength) {
		gl.glPushAttrib(GL.GL_CURRENT_BIT);
		draw(gl);
		if (drawVertexNormal) {
			gl.glColor4d(1., 0., 0., 1.);
			for (Vertex vertex : vertices)
				vertex.drawNormal(gl, normalLength);
		}
		if (drawEdgeNormal) {
			gl.glColor4d(0., 1., 0., 1.);
			for (Edge edge : edgeDB.getEdges())
				edge.drawNormal(gl, normalLength);
		}
		if (drawFaceNormal) {
			gl.glColor4d(0., 0., 1., 1.);
			for (Face face : faces)
				face.drawNormal(gl, normalLength);
		}
		gl.glPopAttrib();
	}
	
	/**
	 * Allows to enable/disable the use of vertex normals (instead of face normals) when drawing the model. Note that
	 * this will be ignored if no normal data was specified when the model was created.
	 * 
	 * @param gl OpenGL object used for drawing
	 * @param useVertexNormals whether to use vertex normals or not when drawing the model
	 */
	public void enableVertexNormals(GL gl, boolean useVertexNormals) {
		displayList_current = useVertexNormals ? displayList_vertexNormals : displayList_faceNormals;
	}
	
	/**
	 * This method will generate the display lists used for this model.
	 * 
	 * @param gl OpenGL object used for drawing
	 */
	private void generateDisplayLists(GL gl) {
		// display list when using vertex normals
		if (displayList_vertexNormals == 0) {
			displayList_vertexNormals = gl.glGenLists(1);
			gl.glNewList(displayList_vertexNormals, GL.GL_COMPILE);
			switch (meshType) {
				case MESH_TRIANGLES:
					gl.glBegin(GL.GL_TRIANGLES);
					for (Face face : faces)
						face.glEvaluate(gl, false);
					gl.glEnd();
					break;
				
				case MESH_TRIANGLE_STRIP:
					gl.glBegin(GL.GL_TRIANGLE_STRIP);
					for (Vertex vertex : vertices)
						vertex.glEvaluate(gl, true);
					gl.glEnd();
					break;
				
				case MESH_QUADS:
					gl.glBegin(GL.GL_QUADS);
					for (Face face : faces)
						face.glEvaluate(gl, false);
					gl.glEnd();
					break;
				
				case MESH_MIXED:
				default:
					gl.glBegin(GL.GL_POLYGON);
					for (Face face : faces)
						face.glEvaluate(gl, false);
					gl.glEnd();
			}
			gl.glEndList();
		}
		
		// display list when using face normals
		if (displayList_faceNormals == 0) {
			displayList_faceNormals = gl.glGenLists(1);
			gl.glNewList(displayList_faceNormals, GL.GL_COMPILE);
			switch (meshType) {
				// triangle strips do not support face normals --> use normal triangles
				case MESH_TRIANGLE_STRIP:
				case MESH_TRIANGLES:
					gl.glBegin(GL.GL_TRIANGLES);
					for (Face face : faces)
						face.glEvaluate(gl, true);
					gl.glEnd();
					break;
				
				case MESH_QUADS:
					gl.glBegin(GL.GL_QUADS);
					for (Face face : faces)
						face.glEvaluate(gl, true);
					gl.glEnd();
					break;
				
				case MESH_MIXED:
				default:
					gl.glBegin(GL.GL_POLYGON);
					for (Face face : faces)
						face.glEvaluate(gl, true);
					gl.glEnd();
			}
			gl.glEndList();
		}
	}
	
	/**
	 * Returns the bounding box of this model (aligned at standard axis in object coordinates).
	 * 
	 * @return bounding box of model (aligned at standard axis in object coordinates)
	 */
	public Cuboid getBoundingBox() {
		return boundingBox;
	}
	
	/**
	 * Initializes the model.
	 * 
	 * @param gl OpenGL object used for drawing
	 * @param whether to use vertex normals instead of face normals
	 */
	public void init(GL gl, boolean useVertexNormals) {
		generateDisplayLists(gl);
		enableVertexNormals(gl, useVertexNormals);
	}
}
